<template>
  <div ref="anchorRef" :class="[ns.b(), ns.m(type), showBackground && ns.m('show-background')]">
    <div v-if="type == 'rail'" :class="ns.e('rail')" />
    <div ref="barRef" :class="ns.e('bar')" />
    <slot />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, computed, watch, watchPostEffect, onMounted, onBeforeUnmount, ExtractPropTypes, PropType, provide } from 'vue'
import { unrefElement, MaybeComputedElementRef } from '@vueuse/core'
import { getScrollParent } from '../_util/Dom/getScrollParent'
import { useNameSpace } from '../_util/hooks'
import { throttle } from '../_util/_'
import { AnchorContextKey, AnchorLinkContextProps } from './context'
import { scroller as Scroller } from './utils'

const preCls = 'x-anchor'

const anchorProps = {
  value: String,
  type: { type: String as PropType<'rail' | 'block'>, default: 'rail' },
  showBackground: { type: Boolean, default: true },
  offset: { type: Number, default: 0 },
  container: [String, Object, Function] as PropType<string | MaybeComputedElementRef>,
  scrollDuration: { type: Number, default: 350 }
}

export type AnchorProps = ExtractPropTypes<typeof anchorProps>

export default defineComponent({
  name: preCls,
  props: anchorProps,
  emits: ['change'],
  setup(props, { emit }) {
    const ns = useNameSpace(preCls)
    const anchorRef = ref<HTMLElement>()
    const barRef = ref<HTMLElement>()

    const activeRef = ref('')
    const links = [] as AnchorLinkContextProps[]

    watch(activeRef, v => emit('change', v))

    watchPostEffect(() => {
      // It maybe null in ssr
      if (!barRef.value) return
      const el = links.find(e => e.href == activeRef.value)?.el
      if (el) {
        const boundTop = anchorRef.value.getBoundingClientRect().top
        const rect = el.getBoundingClientRect()
        const h = rect.height
        barRef.value.style.height = h + 'px'
        barRef.value.style.width = '100%'
        barRef.value.style.top = rect.top - boundTop + (h >> 1) + 'px'
      } else {
        barRef.value.style.height = '0px'
        barRef.value.style.width = '0px'
      }
    })

    provide(AnchorContextKey, {
      value: activeRef,
      links,
      setActive,
      props
    })

    const container = computed(() => {
      return props.container ? (typeof props.container == 'string' ? document.querySelector(props.container) : unrefElement(props.container)) : getScrollParent(anchorRef.value)
    })
    const onScroll = throttle(computeCurrentLink, 128)
    const addListener = () => {
      container.value?.addEventListener('scroll', onScroll, { passive: true })
    }
    const delListener = () => {
      container.value.removeEventListener('scroll', onScroll)
      onScroll.cancel()
    }

    onMounted(() => {
      watch(
        container,
        (val, old) => {
          old?.removeEventListener('scroll', onScroll)
          addListener()
        },
        { immediate: true, flush: 'post' }
      )
    })
    onBeforeUnmount(() => {
      delListener()
    })

    function getBoundTop() {
      return container.value.nodeType != 9 ? container.value.getBoundingClientRect().top : 0
    }

    const scrollEl = computed(() => (container.value.nodeType == 9 ? document.documentElement : (container.value as HTMLElement)))
    const scroller = Scroller(scrollEl, 350, delListener, addListener)

    function setActive(href: string) {
      activeRef.value = href
      const el = container.value.querySelector(href)
      if (!el) return
      const rect = el.getBoundingClientRect()
      const y = scrollEl.value.scrollTop + (rect.top - getBoundTop()) - props.offset + 1
      scroller.scroll({ y })
      location.hash = encodeURIComponent(href.replace(/^#/, ''))
    }

    function computeCurrentLink() {
      if (scroller.transiting) return
      const { offset } = props
      const boundTop = getBoundTop()
      const infos = links
        .map(e => {
          const el = container.value.querySelector(e.href)
          return {
            href: e.href,
            rect: el?.getBoundingClientRect()
          }
        })
        .filter(e => e.rect)
        .sort((a, b) => a.rect.top - b.rect.top)

      let info
      for (let i = 0; i < infos.length; i++) {
        if (infos[i].rect.top <= boundTop + offset) info = infos[i]
        else break
      }
      activeRef.value = info?.href
    }

    return {
      ns,
      anchorRef,
      barRef
    }
  }
})
</script>
